Skip to content

Conversation

@AdamFelt
Copy link
Contributor

This submission fixes render buffer handling between renderers and frame passes.

Removes the CopyTask and moves copy functionality to the RenderBufferManager.
Moves AOVInput and RenderTask gathering out of the update function and puts it in the render loop.
Texture output contents are now copied to the next pass buffer inputs between passes, not as part of the pass.
RenderBuffers are reused from the previous pass not the first pass which supports more flexible mixing of renderers.
New render buffers are created if the render switches between passes so renderers are no longer rendering into a render buffer they did not create. RenderBuffers are still reused from previous passes if the renderer does not change.

Copy link
Contributor

@sebastienberube-adsk sebastienberube-adsk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a bunch of observation comments to understand and document the PR.
You can ignore most of them, but maybe these points stand out a little more:

  • Use of TkToken instead of string for RenderBufferBinding::rendererName
  • Removal of CreateRenderBufferProxy class in framePassUtils.h/cpp
    • I believe the class isn't relevant anymore, since the RenderBufferManager now supports scenarios with mixed render delegates. As far as I know, the CreateRenderBufferProxy class was only added to support these scenarios, which is now redundant.
  • Add check or assert to make sure the colorInput and colorDesc are for a "color" aov
    (I believe there is an implicit assumption the color is always first, but just in case, I would add something to double-check- and protect this assumption against future changes.
    See in RenderBufferManager.cpp (ln :558-562)
                else if (!colorInput.texture)
                {
                    colorDesc  = desc;
                    colorInput = input;
                }

HdAovTokens->instanceId },
{},
_passParams.renderParams.viewport); // NOTE: this is the non-adjusted viewport.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never understood the exact reason why initializing AOV buffers was necessary when using a renderer other than Storm.

I can see similar code being executed with the TaskController, from the constructor:
HdxTaskController::HdxTaskController -> HdxTaskController::_CreateRenderGraph() -> HdxTaskController::SetRenderOutputs("color")

Since I do not understand why this line had been added to the TaskController, I am slightly worried removing it here might break something.

However, on the other hand, I can see that _bufferManager->SetRenderOutputs is called early, in the beggining of FramePass::GetRenderTasks. Furthermore, all task update code (the commit functions) are called at the very end of FramePass::GetRenderTasks, which means not much is happening before SetRenderOutputs is called. So maybe it does not change anything to make this call later, given the order of things with the FramePass.

In conclusion, removing _bufferManager->SetRenderOutputs from this task creation function in the FramePass is probably safe, and this call might only be needed with the TaskController because it has a different task parameter update sequence, requiring the early call to HdxTaskController::SetRenderOutputs("color").

See:
https://git.autodesk.com/autodesk-forks/USD/blob/d34c093b8126e5b82fa35b478586539f82ceadd7/pxr/imaging/hdx/taskController.cpp#L292C13-L292C29

hvt::RenderBufferBindings FramePass::GetRenderBufferBindingsForNextPass(
std::vector<pxr::TfToken> const aovs, bool copyContents)
{
std::string renderName = GetRenderIndex()->GetRenderDelegate()->GetRendererDisplayName();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::string renderName = GetRenderIndex()->GetRenderDelegate()->GetRendererDisplayName();
std::string const& renderName = GetRenderIndex()->GetRenderDelegate()->GetRendererDisplayName();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, a TfToken would be preferable here I think. The TfToken for the rendererName could be stored in the FramePass of the RenderBufferManager in FramePass::Initialize.

HdAovDescriptorList outputDescs;

// NOTE: A function could be used to get localOutputs.
TfTokenVector localOutputs = outputs;
Copy link
Contributor

@sebastienberube-adsk sebastienberube-adsk Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Observation]

This is out of scope for this PR since the name did not change, but we could rename localOutputs to something more descriptive of what it actually is, something like supportedOutputs maybe, since the only difference that I see between outputs and localOutputs is that each entry is validated as shown below:

for (auto it = localOutputs.begin(); it != localOutputs.end();)
{
    HdAovDescriptor desc = _pRenderIndex->GetRenderDelegate()->GetDefaultAovDescriptor(*it);
    if (desc.format == HdFormatInvalid)
    {
        // The backend doesn't support this AOV, so skip it.
        it = localOutputs.erase(it);
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that "outputs" is a very confusing name, as indeed it is an input to the render pass. To me, "local" is ok, or maybe better yet "temp", because they are what is being computed in this function. "Supported" is ok too and is actually what it is filtering on. I don't want to change this now though because this PR is already too big.


// If progressive rendering is enabled, render buffer clear is only required when `_aovOutputs
// != outputs`.
bool needClear = !_isProgressiveRenderingEnabled || _aovOutputs != outputs;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Observation]

bool needClear = !_isProgressiveRenderingEnabled || _aovOutputs != outputs;

I see this was moved below and is only executed if (somethingChanged) which should be functionally the same as before, with the early return if cached inputs and outputs have changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we just can't bail out early because we need to do the copy, but the functionality should still be skipped.

{
std::copy(inputs.begin(), inputs.end(), back_inserter(_aovInputs));
_viewportAov = TfToken();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this block was also moved below and is only executed if (somethingChanged), which makes sense and should deliver the same outcome as before:

     _aovInputs.clear();
    if (inputs.size() > 0)
    {
        std::copy(inputs.begin(), inputs.end(), back_inserter(_aovInputs));
        _viewportAov = TfToken();
    }

}
if (colorInput.texture)
{
PrepareBuffersFromInputs(colorInput, depthInput, colorDesc, controllerId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Observation]

My understanding is this function replaces the CopyTask.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Comment on lines +616 to +618
// Note, it would be better to just assign the output buffer here, but this breaks some
// unit tests that expect this to be null and do a pointer-as-string comparison if it is not
// which is not easily fixable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Question]

Which test is this breaking?
(If the test is TestTaskParameters, I could probaly do something about it)

In any case, this appears like a minor thing, won't block the PR for this, I am mostly curious of the test this would break.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two tests that compare the task parameters. They just use the io << functionality so the pointer is written out deep in Hydra.

Comment on lines -398 to -410
HdRenderBuffer* firstInput = nullptr;
HdRenderBuffer* depthInput = nullptr;
HdRenderBuffer* neyeInput = nullptr;
for (auto input : inputs)
{
// This is super fragile and limited.
// We should be able to pass more inputs to other passes.
if (input.first == localOutputs[0])
firstInput = input.second;
if (input.first == "depth")
depthInput = input.second;
if (input.first == HdAovTokens->Neye)
neyeInput = input.second;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Observation]

I am guessing this part, where the 3 inputs are collected before calling SetViewportRenderOutput, is not longer relevant since the function calls GetRenderOutput(color/depth/Neye) internally to get each one of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is no longer needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad this was removed. Less complexity.

PXR_NS::TfToken aovName;
PXR_NS::HgiTextureHandle texture;
PXR_NS::HdRenderBuffer* buffer;
std::string rendererName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: use a TfToken for the renderName instead of a string.

Since TfToken creation is somewhat expensive, it could be created once in FramePass::Initialize(), then stored as a FramePass or RenderBufferManager member variable and assigned in the RenderBufferBinding without having to deal with the string representation after FramePass::Initialize().

Comparisons and assignments of TfTokens are cheap, once converted from a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, could be. This is not executed in a deep or large loop so there will be no real performance improvement. I wish the name from Hydra was a token to begin with, or there was a way to get the more accurate name "HdStormRenderDelegate". We could store it in the frame pass, but I would like to make this change in a second PR not this one.

/// Returns the default selection accessor.
SelectionSettingsProviderWeakPtr GetSelectionSettingsAccessor() const;

// Returns true if the frame pass should be rendered.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful Doxygen comments are /// (and not //.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this.

{
PXR_NS::TfToken aovName;
PXR_NS::HgiTextureHandle texture;
PXR_NS::HdRenderBuffer* buffer;
Copy link
Contributor

@hodoulp hodoulp Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PXR_NS::HdRenderBuffer* buffer; -> PXR_NS::HdRenderBuffer* buffer = nullptr;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I will fix.

if (input.aovName == localOutputs[i])
{
foundInput = input.second;
inputFound = (rendererName == input.rendererName);
Copy link
Contributor

@hodoulp hodoulp Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the name comparison enough?

The difference is not on the render delegate name but rather on CPU vs. GPU and texture formats. For example, if two different render delegates are both GPU & same texture formats, the code should not add extra copy (ies). I assume that it will be the case between Flash and Storm for example.

ctx->erase(HdAovTokens->depth);
ctx->erase(HdxAovTokens->colorIntermediate);
ctx->erase(HdAovTokens->Neye);
ctx->erase(msaaToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebastienberube-adsk It seems that the neye rework remains. That could be a in Q4 dedicated backlog as discussed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't touch Neye. It still needs to be fixed. It should not be on the context. The context is for things that are read and rewritten, not just read.

}

hvt::RenderBufferBindings FramePass::GetRenderBufferBindingsForNextPass(
std::vector<pxr::TfToken> const aovs, bool copyContents)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::vector<pxr::TfToken> const aovs -> std::vector<pxr::TfToken> const& aovs

@AdamFelt AdamFelt merged commit 1dd9b71 into main Oct 28, 2025
10 checks passed
@AdamFelt AdamFelt deleted the felta/Improve-Buffer-Transition branch October 28, 2025 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants